﻿#region File and License Information
/*
<File>
	<License Type="BSD">
		Copyright © 2009 - 2012, Outcoder. All rights reserved.
	
		This file is part of Calcium (http://calciumsdk.net).

		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions are met:
			* Redistributions of source code must retain the above copyright
			  notice, this list of conditions and the following disclaimer.
			* Redistributions in binary form must reproduce the above copyright
			  notice, this list of conditions and the following disclaimer in the
			  documentation and/or other materials provided with the distribution.
			* Neither the name of the <organization> nor the
			  names of its contributors may be used to endorse or promote products
			  derived from this software without specific prior written permission.

		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
		ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
		WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
		DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
		DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
		(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
		LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
		ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	</License>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com" />
	<CreationDate>2009-03-01 18:44:00Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Security;
using System.Threading;

using Outcoder.Concurrency;
using Outcoder.Logging;

namespace Outcoder.ServiceModel
{
	public static class InstanceIdHeader
	{
		public static readonly string HeaderName = "InstanceId";
		public static readonly string HeaderNamespace = OrganizationalConstants.ServiceContractNamespace;
	}

	/// <summary>
	/// Default implementation of the <see cref="IChannelManager"/>.
	/// Contains methods that enable the creation of channels that implement
	/// specific service interfaces. This class is designed to be used with
	/// Unity in order for transparent factory production of service channels.
	/// This class can be used to provide Unity with a delegate
	/// for creating channels implementing the specified service interface.
	/// </summary>
	public class ChannelManagerSingleton : IChannelManager
	{
		Guid instanceId = Guid.NewGuid();

		#region Singleton implementation

		ChannelManagerSingleton()
		{
		}

		public static ChannelManagerSingleton Instance
		{
			get
			{
				return Nested.instance;
			}
		}

		class Nested
		{
			// Explicit static constructor to tell C# compiler
			// not to mark type as beforefieldinit
			static Nested()
			{
			}

			internal static readonly ChannelManagerSingleton instance = new ChannelManagerSingleton();
		}

		#endregion

		//readonly LoggerSingleton log = LoggerSingleton.Instance;

		readonly Dictionary<Type, object> channels = new Dictionary<Type, object>();
		/* LockRecursionPolicy.SupportsRecursion may not be required. It has been noticed 
		 * that recursion is occuring occasionally, and SupportsRecursion has been assigned 
		 * to prevent this problem. In short, it's a hack for now. */
		readonly ReaderWriterLockSlim channelsLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
		readonly Dictionary<DuplexChannelKey, object> duplexChannels = new Dictionary<DuplexChannelKey, object>();
		readonly ReaderWriterLockSlim duplexChannelsLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
		readonly string channelIdentifier = Assembly.GetExecutingAssembly().GetName().Name;

//		/// <summary>
//		/// Creates or retrieves a cached service channel.
//		/// <example>
//		/// var staticFactoryConfiguration = unityContainer.Configure&lt;IStaticFactoryConfiguration&gt;();
//		///	Debug.Assert(staticFactoryConfiguration != null);
//		///	if (staticFactoryConfiguration != null)
//		///	{
//		///		staticFactoryConfiguration.RegisterFactory&lt;IYourService&gt;(
//		///			ChannelManagerSingleton.Instance.GetChannel&lt;IYourService&gt;);
//		/// 
//		///		/* Attempt to retrieve and use the service. */
//		///		var yourService = Dependency.Resolve&lt;IYourService&gt;();
//		///		yourService.DoSomething();
//		/// }
//		/// </example>
//		/// </summary>
//		/// <param name="unityContainer">The unity container.</param>
//		/// <returns>A channel of the specified type.</returns>
//		/// <exception cref="CommunicationException">Occurs if the service implements <see cref="IServiceContract"/>, 
//		/// and the call to InitiateConnection results in a <code>CommunicationException</code></exception>
//		/// <exception cref="SecurityNegotiationException">Occurs if the service implements <see cref="IServiceContract"/>, 
//		/// and the call to InitiateConnection results in a <code>SecurityNegotiationException</code></exception>
//		public object GetChannel<TChannel>(IUnityContainer unityContainer)
//		{
//			return GetChannel<TChannel>();
//		}

		public TChannel GetChannel<TChannel>()
		{
			Type serviceType = typeof(TChannel);
			object service;

			bool cached;
			channelsLock.EnterUpgradeableReadLock();
			try
			{
				cached = channels.TryGetValue(serviceType, out service);
				if (!cached)
				{	/* Value not in cache, therefore we create it. */
					channelsLock.EnterWriteLock();
					try
					{
						cached = channels.TryGetValue(serviceType, out service);
						if (!cached)
						{
							BindingEndpointPair pair = null;
							codeEndpointsLock.EnterReadLock();
							try
							{
								codeEndpoints.TryGetValue(serviceType, out pair);
							}
							finally
							{
								codeEndpointsLock.ExitReadLock();
							}
							
							ChannelFactory<TChannel> channelFactory;

							/* We don't cache the factory as it contains a list of channels 
							 * that aren't removed if a fault occurs. */
							if (pair != null)
							{
								channelFactory = new ChannelFactory<TChannel>(
														pair.Binding, pair.Address);
							}
							else
							{
								channelFactory = new ChannelFactory<TChannel>("*");
							}

							service = channelFactory.CreateChannel();
							AddCustomHeaders((IClientChannel)service);

							var communicationObject = (ICommunicationObject)service;
							communicationObject.Faulted += OnSimplexChannelFaulted;
							communicationObject.Closing += OnSimplexChannelClosing;
							channels.Add(serviceType, service);
							communicationObject.Open();
							ConnectIfClientService(service, serviceType);

							if (Dependency.Initialized)
							{
								Dependency.Register<TChannel>((TChannel)service);
							}
						}
					}
					finally
					{
						channelsLock.ExitWriteLock();
					}
				}
			}
			finally
			{
				channelsLock.ExitUpgradeableReadLock();
			}

			if (cached)
			{
				try
				{
					ConnectIfClientService(service, serviceType);
				}
				catch (Exception ex)
				{
					ILog log = ResolveLog();
					if (log != null)
					{
						log.Warn("Possible timeout occured. Disconnecting and recreating channel " + serviceType, ex);
					}
					/* Disconnecting the channel will cause it to be removed from the cache 
					 * and thus recreated when GetChannel is next called. This effectively causes a retry. */
					DisconnectSimplexChannel((ICommunicationObject)service);

					return GetChannel<TChannel>();
				}
			}
			return (TChannel)service;
		}

		void AddCustomHeaders(IClientChannel channel)
		{
			MessageHeader shareableInstanceContextHeader = MessageHeader.CreateHeader(
				InstanceIdHeader.HeaderName,
				InstanceIdHeader.HeaderNamespace,
				instanceId.ToString());

			var scope = new OperationContextScope(channel);
			OperationContext.Current.OutgoingMessageHeaders.Add(shareableInstanceContextHeader);
		}

		public TChannel GetDuplexChannel<TChannel>(object callbackInstance)
		{
			ArgumentValidator.AssertNotNull(callbackInstance, "callbackInstance");

			Type serviceType = typeof(TChannel);
			object service;
			var key = new DuplexChannelKey { ServiceType = serviceType, CallBackInstance = callbackInstance };

			bool cached;
			duplexChannelsLock.EnterUpgradeableReadLock();
			try
			{
				cached = duplexChannels.TryGetValue(key, out service);
				if (!cached)
				{	/* Value not in cache, therefore we create it. */
					duplexChannelsLock.EnterWriteLock();
					try
					{
						cached = duplexChannels.TryGetValue(key, out service);
						if (!cached)
						{

							var context = new InstanceContext(callbackInstance);

							BindingEndpointPair pair = null;
							codeEndpointsLock.EnterReadLock();
							try
							{
								codeEndpoints.TryGetValue(serviceType, out pair);
							}
							finally
							{
								codeEndpointsLock.ExitReadLock();
							}
							
							DuplexChannelFactory<TChannel> channelFactory;

							/* We don't cache the factory as it contains a list of channels 
							 * that aren't removed if a fault occurs. */
							if (pair != null)
							{
								channelFactory = new DuplexChannelFactory<TChannel>(context, pair.Binding, pair.Address);
							}
							else
							{
								channelFactory = new DuplexChannelFactory<TChannel>(context, "*");
							}

							service = channelFactory.CreateChannel();
							AddCustomHeaders((IClientChannel)service);
							var communicationObject = (ICommunicationObject)service;
							communicationObject.Faulted += OnDuplexChannelFaulted;
							communicationObject.Closing += OnDuplexChannelClosing;

							duplexChannels.Add(key, service);
							try
							{
								communicationObject.Open();
							}
							catch (Exception ex)
							{
								throw new CommunicationException("Unable to open channel " + typeof(TChannel), ex);
							}

							ConnectIfClientService(service, serviceType);

							Dependency.Register<TChannel>((TChannel)service);
						}
					}
					finally
					{
						duplexChannelsLock.ExitWriteLock();
					}
				}
			}
			finally
			{
				duplexChannelsLock.ExitUpgradeableReadLock();
			}
					  
			if (cached)
			{
				/* Test that channel is still valid. */
				try
				{
					ConnectIfClientService(service, serviceType);
				}
				catch (Exception ex)
				{
					ILog log = ResolveLog();
					if (log != null)
					{
						log.Warn("Possible timeout occured. Disconnecting and recreating channel " + serviceType, ex);
					}
					/* Disconnecting the channel will cause it to be removed from the cache 
					 * and thus recreated when GetChannel is next called. This effectively causes a retry. */
					DisconnectDuplexChannel((ICommunicationObject)service);

					return GetDuplexChannel<TChannel>(callbackInstance);
				}
			}
			return (TChannel)service;
		}

		/// <summary>
		/// Attempts to excercise the <code>IServiceContract.InitiateConnection</code> method
		/// on the specified service if that service is an <see cref="IServiceContract"/>.
		/// </summary>
		/// <param name="service">The service to attempt a connection.</param>
		/// <param name="serviceType">Type of the service for logging purposes.</param>
		/// <exception cref="CommunicationException">Occurs if the service implements <see cref="IServiceContract"/>, 
		/// and the call to InitiateConnection results in a <code>CommunicationException</code></exception>
		/// <exception cref="SecurityNegotiationException">Occurs if the service implements <see cref="IServiceContract"/>, 
		/// and the call to InitiateConnection results in a <code>SecurityNegotiationException</code></exception>
		void ConnectIfClientService(object service, Type serviceType)
		{
			var clientService = service as IServiceContract;
			if (clientService != null)
			{
				ILog log = ResolveLog();

				string response;
				try
				{
					response = clientService.InitiateConnection(channelIdentifier);
				}
				catch (Exception ex)
				{
					
					log?.Warn("Unable to connect to IServiceContract instance. Type: " + serviceType, ex);

					throw;
				}

				log?.Info(string.Format("Received response from {0} \"{1}\"", serviceType, response));
			}
		}

		void OnSimplexChannelClosing(object sender, EventArgs e)
		{
			ILog log = ResolveLog();
			log?.Debug("Simplex channel closed.");
			DisconnectSimplexChannel((ICommunicationObject)sender);
		}

		/// <summary>
		/// Called when a channel fault occurs. The channel is removed from the channels cache
		/// so that the next request to retrieve the channel will see it recreated.
		/// </summary>
		/// <param name="sender">The <see cref="ICommunicationObject"/> sender.</param>
		/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		void OnSimplexChannelFaulted(object sender, EventArgs e)
		{
			ILog log = ResolveLog();
			log?.Warn("Simplex channel faulted " + sender);
			DisconnectSimplexChannel((ICommunicationObject)sender);
		}

		void DisconnectSimplexChannel(ICommunicationObject communicationObject)
		{
			channelsLock.EnterWriteLock();
			try
			{
				communicationObject.Faulted -= OnSimplexChannelFaulted;
				communicationObject.Closing -= OnSimplexChannelClosing;

				try
				{
					communicationObject.Abort();
				}
				catch (Exception ex)
				{
					ILog log = ResolveLog();
					log?.Warn("Unable to abort faulted service.", ex);
				}

				var keys = from pair in channels
						   where pair.Value == communicationObject
						   select pair.Key;

				/* Remove all items matching the channel. 
				 * This is somewhat defensive as there should only be one instance 
				 * of the channel in the channel dictionary. */
				foreach (var key in keys.ToList())
				{
					channels.Remove(key);
				}
			}
			finally
			{
				channelsLock.ExitWriteLock();
			}
		}

		void OnDuplexChannelClosing(object sender, EventArgs e)
		{
			ILog log = ResolveLog();
			log?.Debug("Duplex channel closed.");
			DisconnectDuplexChannel((ICommunicationObject)sender);
		}

		void OnDuplexChannelFaulted(object sender, EventArgs e)
		{
			ILog log = ResolveLog();
			log?.Warn("Duplex channel faulted.");
			DisconnectDuplexChannel((ICommunicationObject)sender);
		}

		void DisconnectDuplexChannel(ICommunicationObject communicationObject)
		{
			duplexChannelsLock.EnterWriteLock();
			try
			{
				communicationObject.Faulted -= OnDuplexChannelFaulted;
				communicationObject.Closing -= OnDuplexChannelClosing;

				try
				{
					communicationObject.Abort();
				}
				catch (Exception ex)
				{
					ILog log = ResolveLog();
					log?.Warn("Unable to abort faulted service.", ex);
				}

				var keys = from pair in duplexChannels
						   where pair.Value == communicationObject
						   select pair.Key;

				/* Remove all items matching the channel. 
				 * This is somewhat defensive as there should only be one instance 
				 * of the channel in the channel dictionary. */
				foreach (var key in keys.ToList())
				{
					duplexChannels.Remove(key);
				}
			}
			finally
			{
				duplexChannelsLock.ExitWriteLock();
			}
		}

		class DuplexChannelKey
		{
			public object CallBackInstance { get; set; }
			public Type ServiceType { get; set; }

			public override bool Equals(object obj)
			{
				if (ReferenceEquals(null, obj))
				{
					return false;
				}
				if (ReferenceEquals(this, obj))
				{
					return true;
				}
				if (obj.GetType() != typeof(DuplexChannelKey))
				{
					return false;
				}
				return Equals((DuplexChannelKey)obj);
			}

			public bool Equals(DuplexChannelKey obj)
			{
				if (ReferenceEquals(null, obj))
				{
					return false;
				}
				if (ReferenceEquals(this, obj))
				{
					return true;
				}
				return Equals(obj.CallBackInstance, CallBackInstance) && Equals(obj.ServiceType, ServiceType);
			}

			public override int GetHashCode()
			{
				unchecked
				{
					return ((CallBackInstance != null ? CallBackInstance.GetHashCode() : 0) * 397)
						^ (ServiceType != null ? ServiceType.ToString().GetHashCode() : 0);
				}
			}

			public override string ToString()
			{
				if (CallBackInstance != null)
				{
					return string.Format("{0} {1}, CallBackInstance HashCode: {2}", 
						ServiceType, CallBackInstance, CallBackInstance.GetHashCode());
				}
				return string.Format("{0} {1}", ServiceType, CallBackInstance);
			}
		}

		#region Code Endpoints

		readonly Dictionary<Type, BindingEndpointPair> codeEndpoints = new Dictionary<Type, BindingEndpointPair>();
		readonly ReaderWriterLockSlim codeEndpointsLock = new ReaderWriterLockSlim();

		public void SetBinding(Type channelType, Binding binding, EndpointAddress endPointAddress)
		{
			ArgumentValidator.AssertNotNull(channelType, "channelType");
			ArgumentValidator.AssertNotNull(binding, "binding");
			ArgumentValidator.AssertNotNull(endPointAddress, "endPointAddress");

			codeEndpointsLock.EnterWriteLock();
			try
			{
				codeEndpoints[channelType] = new BindingEndpointPair(binding, endPointAddress);
			}
			finally
			{
				codeEndpointsLock.ExitWriteLock();
			}

			duplexChannelsLock.EnterWriteLock();
			try
			{
				foreach (var duplexChannel in duplexChannels.ToList())
				{
					if (duplexChannel.Key.ServiceType == channelType)
					{
						duplexChannels.Remove(duplexChannel.Key);
					}
				}
			}
			finally
			{
				duplexChannelsLock.ExitWriteLock();
			}

			channelsLock.EnterWriteLock();
			try
			{
				channels.Remove(channelType);
			}
			finally
			{
				channelsLock.ExitWriteLock();
			}
		}

		class BindingEndpointPair
		{
			public Binding Binding { get; private set; }
			public EndpointAddress Address { get; private set; }

			public BindingEndpointPair(Binding binding, EndpointAddress address)
			{
				Binding = binding;
				Address = address;
			}
		}

		#endregion

		static ILog ResolveLog()
		{
			ILog result;
			Dependency.TryResolve(out result);
			return result;
		}
	}
}
